﻿using BizHawk.Common;

namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
    /// <summary>
    /// Definitions
    /// </summary>
    /*
        Implementation based on the information contained here:
        http://www.cpcwiki.eu/index.php/765_FDC
        and here:
        http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf
    */
    public partial class NECUPD765
    {
        /// <summary>
        /// Defines the current phase of the controller
        /// </summary>
        private enum Phase
        {
            /// <summary>
            /// FDC is in an idle state, awaiting the next initial command byte
            /// </summary>
            Idle,

            /// <summary>
            /// FDC is in a state waiting for the next command instruction
            /// A command consists of a command byte (eventually including the MF, MK, SK bits), and up to eight parameter bytes
            /// </summary>
            Command,

            /// <summary>
            /// During this phase, the actual data is transferred (if any). Usually that are the data bytes for the read/written sector(s), except for the Format Track Command,
            /// in that case four bytes for each sector are transferred
            /// </summary>
            Execution,

            /// <summary>
            /// Returns up to seven result bytes (depending on the command) that are containing status information. The Recalibrate and Seek Track commands do not return result bytes directly,
            /// instead the program must wait until the Main Status Register signalizes that the command has been completed, and then it must (!) send a
            /// Sense Interrupt State command to 'terminate' the Seek/Recalibrate command.
            /// </summary>
            Result
        }

        /// <summary>
        /// The lifecycle of an instruction
        /// Similar to phase, this describes the current 'sub-phase' we are in when dealing with an instruction
        /// </summary>
        private enum InstructionState
        {
            /// <summary>
            /// FDC has received a command byte and is currently reading parameter bytes from the data bus
            /// </summary>
            ReceivingParameters,

            /// <summary>
            /// All parameter bytes have been received. This phase allows any neccessary setup before instruction execution starts
            /// </summary>
            PreExecution,

            /// <summary>
            /// The start of instruction execution. This may end up with the FDC moving into result phase,
            /// but also may also prepare the way for further processing to occur later in execution phase
            /// </summary>
            StartExecute,

            /// <summary>
            /// Data is read or written in execution phase
            /// </summary>
            ExecutionReadWrite,

            /// <summary>
            /// Execution phase is well under way. This state primarily deals with data transfer between CPU and FDC
            /// </summary>
            ExecutionWrite,

            /// <summary>
            /// Execution phase is well under way. This state primarily deals with data transfer between FDC and CPU
            /// </summary>
            ExecutionRead,

            /// <summary>
            /// Execution has finished and results bytes are ready to be read by the CPU
            /// Initial result setup
            /// </summary>
            StartResult,

            /// <summary>
            /// Result processing
            /// </summary>
            ProcessResult,

            /// <summary>
            /// Results are being sent
            /// </summary>
            SendingResults,

            /// <summary>
            /// Final cleanup tasks when the instruction has fully completed
            /// </summary>
            Completed
        }

        /// <summary>
        /// Represents internal interrupt state of the FDC
        /// </summary>
        public enum InterruptState
        {
            /// <summary>
            /// There is no interrupt
            /// </summary>
            None,
            /// <summary>
            /// Execution interrupt
            /// </summary>
            Execution,
            /// <summary>
            /// Result interrupt
            /// </summary>
            Result,
            /// <summary>
            /// Ready interrupt
            /// </summary>
            Ready,
            /// <summary>
            /// Seek interrupt
            /// </summary>
            Seek
        }

        /// <summary>
        /// Possible main states that each drive can be in
        /// </summary>
        public enum DriveMainState
        {
            /// <summary>
            /// Drive is not doing anything
            /// </summary>
            None,
            /// <summary>
            /// Seek operation is in progress
            /// </summary>
            Seek,
            /// <summary>
            /// Recalibrate operation is in progress
            /// </summary>
            Recalibrate,
            /// <summary>
            /// A scan data operation is in progress
            /// </summary>
            Scan,
            /// <summary>
            /// A read ID operation is in progress
            /// </summary>
            ReadID,
            /// <summary>
            /// A read data operation is in progress
            /// </summary>
            ReadData,
            /// <summary>
            /// A read diagnostic (read track) operation is in progress
            /// </summary>
            ReadDiagnostic,
            /// <summary>
            /// A write id (format track) operation is in progress
            /// </summary>
            WriteID,
            /// <summary>
            /// A write data operation is in progress
            /// </summary>
            WriteData,
        }

        /// <summary>
        /// State information during a seek/recalibration operation
        /// </summary>
        public enum SeekSubState
        {
            /// <summary>
            /// Seek hasnt started yet
            /// </summary>
            Idle,
            /// <summary>
            /// Delayed
            /// </summary>
            Wait,
            /// <summary>
            /// Setup for head move
            /// </summary>
            MoveInit,
            /// <summary>
            /// Seek is currently happening
            /// </summary>
            HeadMove,
            /// <summary>
            /// Head move with no delay
            /// </summary>
            MoveImmediate,
            /// <summary>
            /// Ready to complete
            /// </summary>
            PerformCompletion,
            /// <summary>
            /// Seek operation has completed
            /// </summary>
            SeekCompleted
        }

        /// <summary>
        /// Seek int code
        /// </summary>
        public enum SeekIntStatus
        {
            Normal,
            Abnormal,
            DriveNotReady,
        }

        /// <summary>
        /// The direction of a specific command
        /// </summary>
        private enum CommandDirection
        {
            /// <summary>
            /// Data flows from UPD765A to Z80
            /// </summary>
            OUT,
            /// <summary>
            /// Data flows from Z80 to UPD765A
            /// </summary>
            IN
        }

        /// <summary>
        /// Enum defining the different types of result that can be returned
        /// </summary>
        private enum ResultType
        {
            /// <summary>
            /// Standard 7 result bytes are returned
            /// </summary>
            Standard,
            /// <summary>
            /// 1 byte returned - ST3
            /// (used for SenseDriveStatus)
            /// </summary>
            ST3,
            /// <summary>
            /// 1 byte returned - ST0
            /// (used for version &amp; invalid)
            /// </summary>
            ST0,
            /// <summary>
            /// 2 bytes returned for sense interrupt status command
            /// ST0
            /// CurrentCylinder
            /// </summary>
            Interrupt
        }

        /// <summary>
        /// Possible list of encountered drive status errors
        /// </summary>
        public enum Status
        {
            /// <summary>
            /// No error detected
            /// </summary>
            None,
            /// <summary>
            /// An undefined error has been detected
            /// </summary>
            Undefined,
            /// <summary>
            /// Drive is not ready
            /// </summary>
            DriveNotReady,
            /// <summary>
            /// Invalid command received
            /// </summary>
            Invalid,
            /// <summary>
            /// The disk has its write protection tab enabled
            /// </summary>
            WriteProtected,
            /// <summary>
            /// The requested sector has not been found
            /// </summary>
            SectorNotFound
        }

        /// <summary>
        /// Represents the direction that the head is moving over the cylinders
        /// Increment:  Track number increasing (head moving from outside of disk inwards)
        /// Decrement:  Track number decreasing (head moving from inside of disk outwards)
        /// </summary>
        public enum SkipDirection
        {
            Increment,
            Decrement
        }

        // Command Instruction Constants
        // Designates the default postitions within the cmdbuffer array

        public const int CM_HEAD = 0;
        /// <summary>
        /// C - Track
        /// </summary>
        public const int CM_C = 1;
        /// <summary>
        /// H - Side
        /// </summary>
        public const int CM_H = 2;
        /// <summary>
        /// R - Sector ID
        /// </summary>
        public const int CM_R = 3;
        /// <summary>
        /// N - Sector size
        /// </summary>
        public const int CM_N = 4;
        /// <summary>
        /// EOT - End of track
        /// </summary>
        public const int CM_EOT = 5;
        /// <summary>
        /// GPL - Gap length
        /// </summary>
        public const int CM_GPL = 6;
        /// <summary>
        /// DTL - Data length
        /// </summary>
        public const int CM_DTL = 7;
        /// <summary>
        /// STP - Step
        /// </summary>
        public const int CM_STP = 7;

        // Result Instruction Constants
        // Designates the default postitions within the cmdbuffer array

        /// <summary>
        /// Status register 0
        /// </summary>
        public const int RS_ST0 = 0;
        /// <summary>
        /// Status register 1
        /// </summary>
        public const int RS_ST1 = 1;
        /// <summary>
        /// Status register 2
        /// </summary>
        public const int RS_ST2 = 2;
        /// <summary>
        /// C - Track
        /// </summary>
        public const int RS_C = 3;
        /// <summary>
        /// H - Side
        /// </summary>
        public const int RS_H = 4;
        /// <summary>
        /// R - Sector ID
        /// </summary>
        public const int RS_R = 5;
        /// <summary>
        /// N - Sector size
        /// </summary>
        public const int RS_N = 6;

        // Main Status Register Constants
        // Designates the bit positions within the Main status register

        /// <summary>
        /// FDD0 Busy (seek/recalib active, until succesful sense intstat)
        /// FDD number 0 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command.
        /// </summary>
        public const int MSR_D0B = 0;
        /// <summary>
        /// FDD1 Busy (seek/recalib active, until succesful sense intstat)
        /// FDD number 1 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command.
        /// </summary>
        public const int MSR_D1B = 1;
        /// <summary>
        /// FDD2 Busy (seek/recalib active, until succesful sense intstat)
        /// FDD number 2 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command.
        /// </summary>
        public const int MSR_D2B = 2;
        /// <summary>
        /// FDD3 Busy (seek/recalib active, until succesful sense intstat)
        /// FDD number 3 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command.
        /// </summary>
        public const int MSR_D3B = 3;
        /// <summary>
        /// FDC Busy (still in command-, execution- or result-phase)
        /// A Read or Write command is in orocess. (FDC Busy) FDC will not accept any other command
        /// </summary>
        public const int MSR_CB = 4;
        /// <summary>
        /// Execution Mode (still in execution-phase, non_DMA_only)
        /// This bit is set only during execution ohase (Execution Mode) in non-DMA mode When DB5 goes low, execution phase has ended and result phase has started.It operates only during
        ///  non-DMA mode of operation
        /// </summary>
        public const int MSR_EXM = 5;
        /// <summary>
        /// Data Input/Output (0=CPU->FDC, 1=FDC->CPU) (see b7)
        /// Indicates direction of data transfer between FDC and data regrster If DIO = 1, then transfer is from data register to the
        /// processor.If DIO = 0, then transfer is from the processor to data register
        /// </summary>
        public const int MSR_DIO = 6;
        /// <summary>
        /// Request For Master (1=ready for next byte) (see b6 for direction)
        /// ndicates data register IS ready to send or receive data to or from the processor Both bits DIO and RQM should be
        /// used to perform the hand-shaking functions of “ready” and “directron” to the processor
        /// </summary>
        public const int MSR_RQM = 7;

        // Status Register 0 Constants
        // Designates the bit positions within the status register 0

        /// <summary>
        /// Unit Select (driveno during interrupt)
        /// This flag IS used to indicate a drive unit number at interrupt
        /// </summary>
        public const int SR0_US0 = 0;

        /// <summary>
        /// Unit Select (driveno during interrupt)
        /// This flag IS used to indicate a drive unit number at interrupt
        /// </summary>
        public const int SR0_US1 = 1;

        /// <summary>
        /// Head Address (head during interrupt)
        /// State of the head at interrupt
        /// </summary>
        public const int SR0_HD = 2;

        /// <summary>
        /// Not Ready (drive not ready or non-existing 2nd head selected)
        /// Not Ready - When the FDD IS in the not-ready state and a Read or Write command IS Issued, this
        ///  flag IS set If a Read or Write command is issued to side 1 of a single-sided drive, then this flag IS set
        /// </summary>
        public const int SR0_NR = 3;

        /// <summary>
        /// Equipment Check (drive failure or recalibrate failed (retry))
        /// Equipment check - If a fault srgnal IS received from the FDD, or if the track 0 srgnal fails to occur after 77
        ///  step pulses(Recalibrate Command) then this flag is set
        /// </summary>
        public const int SR0_EC = 4;

        /// <summary>
        /// Seek End (Set if seek-command completed)
        /// Seek end - When the FDC completes the Seek command, this flag IS set lo 1 (high)
        /// </summary>
        public const int SR0_SE = 5;

        /// <summary>
        /// Interrupt Code (low byte)
        /// Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd
        /// or senseint with no int occured, 3=aborted:disc removed etc.)
        /// </summary>
        public const int SR0_IC0 = 6;

        /// <summary>
        /// Interrupt Code (high byte)
        /// Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd
        /// or senseint with no int occured, 3=aborted:disc removed etc.)
        /// </summary>
        public const int SR0_IC1 = 7;

        // Status Register 1 Constants
        // Designates the bit positions within the status register 1

        /// <summary>
        /// Missing Address Mark (Sector_ID or DAM not found)
        /// Missing address mark - This bit is set i f the FDC does not detect the IDAM before 2 index pulses It is also set if
        ///  the FDC cannot find the DAM or DDAM after the IDAM i s found.MD bit of ST2 is also set at this time
        /// </summary>
        public const int SR1_MA = 0;

        /// <summary>
        /// Not Writeable (tried to write/format disc with wprot_tab=on)
        /// Not writable (write protect) - During execution of Write Data, Write Deleted Data or Write ID command. if the FDC
        ///  detect: a write protect srgnal from the FDD.then this flag is Set
        /// </summary>
        public const int SR1_NW = 1;

        /// <summary>
        /// No Data
        /// No Data (Sector_ID not found, CRC fail in ID_field)
        ///
        /// During execution of Read Data. Read Deleted Data Write Data.Write Deleted Data or Scan command, if the FDC cannot find
        /// the sector specified in the IDR(2)Register, this flag i s set.
        ///
        /// During execution of the Read ID command. if the FDC cannot read the ID field without an error, then this flag IS set
        ///
        /// During execution of the Read Diagnostic command. if the starting sector cannot be found, then this flag is set
        /// </summary>
        public const int SR1_ND = 2;

        /// <summary>
        /// Over Run (CPU too slow in execution-phase (ca. 26us/Byte))
        /// Overrun - If the FDC i s not serviced by the host system during data transfers within a certain time interval.this flaa i s set
        /// </summary>
        public const int SR1_OR = 4;

        /// <summary>
        /// Data Error (CRC-fail in ID- or Data-Field)
        /// Data error - When the FDC detects a CRC(1) error in either the ID field or the data field, this flag is set
        /// </summary>
        public const int SR1_DE = 5;

        /// <summary>
        /// End of Track (set past most read/write commands) (see IC)
        /// End of cylinder - When the FDC tries to access a sector beyond the final sector of a cylinder, this flag I S set
        /// </summary>
        public const int SR1_EN = 7;

        // Status Register 2 Constants
        // Designates the bit positions within the status register 2

        /// <summary>
        /// Missing Address Mark in Data Field (DAM not found)
        /// Missing address mark - When data IS read from the medium, i f the FDC cannot find a data address mark or deleted
        /// data address mark, then this flag is set
        /// </summary>
        public const int SR2_MD = 0;

        /// <summary>
        /// Bad Cylinder (read/programmed track-ID different and read-ID = FF)
        /// Bad cylinder - This bit i s related to the ND bit. and when the contents of C on the medium is different
        /// from that stored i n the IDR and the contents  of C IS FFH.then this flag IS set
        /// </summary>
        public const int SR2_BC = 1;

        /// <summary>
        /// Scan Not Satisfied (no fitting sector found)
        /// Scan not satisfied - During execution of the Scan command, i f the F D cannot find a sector on the cylinder
        /// which meets the condition.then this flag i s set
        /// </summary>
        public const int SR2_SN = 2;

        /// <summary>
        /// Scan Equal Hit (equal)
        /// Scan equal hit - During execution of the Scan command. i f the condition of “equal” is satisfied, this flag i s set
        /// </summary>
        public const int SR2_SH = 3;

        /// <summary>
        /// Wrong Cylinder (read/programmed track-ID different) (see b1)
        /// Wrong cylinder - This bit IS related to the ND bit, and when  the contents of C(3) on the medium is different
        /// from that stored i n the IDR.this flag is set
        /// </summary>
        public const int SR2_WC = 4;

        /// <summary>
        /// Data Error in Data Field (CRC-fail in data-field)
        /// Data error in data field - If the FDC detects a CRC error i n the data field then this flag is set
        /// </summary>
        public const int SR2_DD = 5;

        /// <summary>
        /// Control Mark (read/scan command found sector with deleted DAM)
        /// Control mark - During execution of the Read Data or Scan command, if the FDC encounters a sector
        ///  which contains a deleted data address mark, this flag is set Also set if DAM is
        ///   found during Read Deleted Data
        /// </summary>
        public const int SR2_CM = 6;

        // Status Register 3 Constants
        // Designates the bit positions within the status register 3

        /// <summary>
        /// Unit select 0
        /// Unit Select (pin 28,29 of FDC)
        /// </summary>
        public const int SR3_US0 = 0;

        /// <summary>
        /// Unit select 1
        /// Unit Select (pin 28,29 of FDC)
        /// </summary>
        public const int SR3_US1 = 1;

        /// <summary>
        /// Head address (side select)
        /// Head Address (pin 27 of FDC)
        /// </summary>
        public const int SR3_HD = 2;

        /// <summary>
        /// Two Side (0=yes, 1=no (!))
        /// Two-side - This bit IS used to indicate the status of the two-side signal from the FDD
        /// </summary>
        public const int SR3_TS = 3;

        /// <summary>
        /// Track 0 (on track 0 we are)
        /// Track 0 - This bit IS used to indicate the status of the track 0 signal from the FDD
        /// </summary>
        public const int SR3_T0 = 4;

        /// <summary>
        /// Ready - status of the ready signal from the fdd
        /// Ready (drive ready signal)
        /// </summary>
        public const int SR3_RY = 5;

        /// <summary>
        /// Write Protected (write protected)
        /// Write protect - status of the wp signal from the fdd
        /// </summary>
        public const int SR3_WP = 6;

        /// <summary>
        /// Fault - This bit is used to indicate the status of the fault signal from the FDD
        /// Fault (if supported: 1=Drive failure)
        /// </summary>
        public const int SR3_FT = 7;

        // Interrupt Code Masks

        /// <summary>
        /// 1 = aborted:readfail / OK if EN (end of track)
        /// </summary>
        public const byte IC_OK = 0x00;

        /// <summary>
        /// 1 = aborted:readfail / OK if EN (end of track)
        /// </summary>
        public const byte IC_ABORTED_RF_OKEN = 0x40;

        /// <summary>
        /// 2 = unknown cmd or senseint with no int occured
        /// </summary>
        public const byte IC_NO_INT_OCCURED = 0x80;

        /// <summary>
        /// 3 = aborted:disc removed etc
        /// </summary>
        public const byte IC_ABORTED_DISCREMOVED = 0xC0;

        // command code constants
        public const int CC_READ_DATA = 0x06;
        public const int CC_READ_ID = 0x0a;
        public const int CC_SPECIFY = 0x03;
        public const int CC_READ_DIAGNOSTIC = 0x02;
        public const int CC_SCAN_EQUAL = 0x11;
        public const int CC_SCAN_HIGHOREQUAL = 0x1d;
        public const int CC_SCAN_LOWOREQUAL = 0x19;
        public const int CC_READ_DELETEDDATA = 0x0c;
        public const int CC_WRITE_DATA = 0x05;
        public const int CC_WRITE_ID = 0x0d;
        public const int CC_WRITE_DELETEDDATA = 0x09;
        public const int CC_SEEK = 0x0f;
        public const int CC_RECALIBRATE = 0x07;
        public const int CC_SENSE_INTSTATUS = 0x08;
        public const int CC_SENSE_DRIVESTATUS = 0x04;
        public const int CC_VERSION = 0x10;
        public const int CC_INVALID = 0x00;

        // drive seek state constants
        public const int SEEK_IDLE = 0;
        public const int SEEK_SEEK = 1;
        public const int SEEK_RECALIBRATE = 2;
        // seek interrupt
        public const int SEEK_INTACKNOWLEDGED = 3;
        public const int SEEK_NORMALTERM = 4;
        public const int SEEK_ABNORMALTERM = 5;
        public const int SEEK_DRIVENOTREADY = 6;

        /// <summary>
        /// Class that holds information about a specific command
        /// </summary>
        private class Command
        {
//          /// <summary>
//          /// Mask to remove potential parameter bits (5,6, and or 7) in order to identify the command
//          /// </summary>
//          public int BitMask { get; set; }
            /// <summary>
            /// The command code after bitmask has been applied
            /// </summary>
            public int CommandCode { get; set; }
            /// <summary>
            /// The number of bytes that make up the full command
            /// </summary>
            public int ParameterByteCount { get; set; }
            /// <summary>
            /// The number of result bytes that will be generated from the command
            /// </summary>
            public int ResultByteCount { get; set; }
            /// <summary>
            /// The command direction
            /// IN - Z80 to UPD765A
            /// OUT - UPD765A to Z80
            /// </summary>
            public CommandDirection Direction { get; set; }
            /// <summary>
            /// Command makes use of the MT bit
            /// </summary>
            public bool MT;
            /// <summary>
            /// Command makes use of the MF bit
            /// </summary>
            public bool MF;
            /// <summary>
            /// Command makes use of the SK bit
            /// </summary>
            public bool SK;
            /// <summary>
            /// Read/Write command that is READ
            /// </summary>
            public bool IsRead;
            /// <summary>
            /// Read/Write command that is WRITE
            /// </summary>
            public bool IsWrite;

            /// <summary>
            /// Delegate function that is called by this command
            /// bool 1: EXECUTE - if TRUE the command will be executed. if FALSE the method will instead parse commmand parameter bytes
            /// bool 2: RESULT - if TRUE
            /// </summary>
            public Action CommandDelegate { get; set; }
        }

        /// <summary>
        /// Storage for command parameters
        /// </summary>
        public class CommandParameters
        {
            /// <summary>
            /// The requested drive
            /// </summary>
            public byte UnitSelect;
            /// <summary>
            /// The requested physical side
            /// </summary>
            public byte Side;
            /// <summary>
            /// The requested track (C)
            /// </summary>
            public byte Cylinder;
            /// <summary>
            /// The requested head (H)
            /// </summary>
            public byte Head;
            /// <summary>
            /// The requested sector (R)
            /// </summary>
            public byte Sector;
            /// <summary>
            /// The specified sector size (N)
            /// </summary>
            public byte SectorSize;
            /// <summary>
            /// The end of track or last sector value (EOT)
            /// </summary>
            public byte EOT;
            /// <summary>
            /// Gap3 length (GPL)
            /// </summary>
            public byte Gap3Length;
            /// <summary>
            /// Data length (DTL) - When N is defined as 00, DTL stands for the data length
            /// which users are going to read out or write into the sector
            /// </summary>
            public byte DTL;

            /// <summary>
            /// Clear down
            /// </summary>
            public void Reset()
            {
                UnitSelect = 0;
                Side = 0;
                Cylinder = 0;
                Head = 0;
                Sector = 0;
                SectorSize = 0;
                EOT = 0;
                Gap3Length = 0;
                DTL = 0;
            }

            public void SyncState(Serializer ser)
            {
                ser.BeginSection("ActiveCmdParams");

                ser.Sync(nameof(UnitSelect), ref UnitSelect);
                ser.Sync(nameof(Side), ref Side);
                ser.Sync(nameof(Cylinder), ref Cylinder);
                ser.Sync(nameof(Head), ref Head);
                ser.Sync(nameof(Sector), ref Sector);
                ser.Sync(nameof(SectorSize), ref SectorSize);
                ser.Sync(nameof(EOT), ref EOT);
                ser.Sync(nameof(Gap3Length), ref Gap3Length);
                ser.Sync(nameof(DTL), ref DTL);

                ser.EndSection();
            }
        }
    }
}
